<?php

    #-------------------------------------------------------------#
	# This file is part of the Tekuna MVC Microframework for PHP. #
	# (c) 2009 Niels Jäckel <niels.jaeckel@tekuna.org>            #
	# Please visit http://tekuna.org/ for more information.       #
	#                                                             #
	# This software is licensed under the MIT License. Please     #
	# view the LICENSE file for the full text of this license.    #
    #-------------------------------------------------------------#


	/**
	 * This class holds several information about the current requesting
	 * client. This covers the client IP, the resolved host, the port, the
	 * user agent, and accepted mime types, languages encodings and character
	 * sets.
	 *
	 * On construction time of the object the values are loaded from the
	 * superglobal $_SERVER array. The host resolving is done lazily when
	 * this value is requested the first time.
	 *
	 * The accept fields are parsed and made available as arrays that are sorted
	 * due to the submitted weights. Variable accept fields (with asterisks) are
	 * resolved implicitly if the methods is*Accepted() are called.
	 */

	class ClientInformation {

		protected
			$sIP = '0.0.0.0',
			$sHost = NULL,
			$iPort = 0,
			$sUserAgent = '',

			$arrAcceptedMimeTypes = array('*/*'),
			$arrAcceptedLanguages = array('*'),
			$arrAcceptedEncodings = array('*'),
			$arrAcceptedCharsets = array('*');


		/**
		 * Create new ClientInformation instance and load the relevant
		 * values from the superglobal $_SERVER array.
		 */

		public function __construct() {

			// import client values
			$this -> sIP = long2ip(ip2long($_SERVER['REMOTE_ADDR'])); // validate email address by back and forward converting
			$this -> iPort = intval($_SERVER['REMOTE_PORT']);
			
			// import user agent, if submitted
			if (isset($_SERVER['HTTP_USER_AGENT'])) {
			
				$this -> sUserAgent = $_SERVER['HTTP_USER_AGENT'];
			}
			
			// import accepted mimetypes, if submitted
			if (isset($_SERVER['HTTP_ACCEPT'])) {
				
				$this -> arrAcceptedMimeTypes = $this -> parseAcceptFields($_SERVER['HTTP_ACCEPT']);
			}

			// import accepted languages, if submitted
			if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {

				$this -> arrAcceptedLanguages = $this -> parseAcceptFields($_SERVER['HTTP_ACCEPT_LANGUAGE']);
			}

			// import accepted encodings, if submitted
			if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])) {

				$this -> arrAcceptedEncodings = $this -> parseAcceptFields($_SERVER['HTTP_ACCEPT_ENCODING']);
			}

			// import accepted charsets, if submitted
			if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) {

				$this -> arrAcceptedCharsets = $this -> parseAcceptFields($_SERVER['HTTP_ACCEPT_CHARSET']);
			}
		}


		/**
		 * @return string returns the client's IP
		 */

		public function getIP() {

			return $this -> sIP;
		}


		/**
		 * @return string returns the client's hostname
		 */

		public function getHost() {

			// lazy loading of the host resolution
			if ($this -> sHost === NULL) {

				$this -> sHost = gethostbyaddr($this -> getIP());
			}

			return $this -> sHost;
		}


		/**
		 * @return integer returns the client's port
		 */

		public function getPort() {

			return $this -> iPort;
		}


		/**
		 * @return string returns the client's user agent
		 */

		public function getUserAgent() {

			return $this -> sUserAgent;
		}


		/**
		 * @return array returns a weight-sorted array of accepted mime types
		 */

		public function getAcceptedMimeTypes() {

			return $this -> arrAcceptedMimeTypes;
		}


		/**
		 * @return array returns a weight-sorted array of accepted languages
		 */

		public function getAcceptedLanguages() {

			return $this -> arrAcceptedLanguages;
		}


		/**
		 * @return array returns a weight-sorted array of accepted encodings
		 */

		public function getAcceptedEncodings() {

			return $this -> arrAcceptedEncodings;
		}


		/**
		 * @return array returns a weight-sorted array of accepted charsets
		 */

		public function getAcceptedCharsets() {

			return $this -> arrAcceptedCharsets;
		}


		/**
		 * Checks if the client accepts the given mime type directly or as
		 * a match of a wildcard accept value
		 *
		 * @param string $sMimeType the checked mime type
		 * @return boolean accepted or not
		 */

		public function isMimeTypeAccepted($sMimeType) {

			$sMimeType = strtolower($sMimeType);

			return in_array($sMimeType, $this -> arrAcceptedMimeTypes)
			       ||
			       $this -> matchAttributePatterns($this -> arrAcceptedMimeTypes, $sMimeType);
		}


		/**
		 * Checks if the client accepts the given language directly or as
		 * a match of a wildcard accept value
		 *
		 * @param string $sLanguage the checked language
		 * @return boolean accepted or not
		 */

		public function isLanguageAccepted($sLanguage) {

			$sLanguage = strtolower($sLanguage);

			return in_array($sLanguage, $this -> arrAcceptedLanguages)
			       ||
			       $this -> matchAttributePatterns($this -> arrAcceptedLanguages, $sLanguage);
		}


		/**
		 * Checks if the client accepts the given encoding directly or as
		 * a match of a wildcard accept value
		 *
		 * @param string $sEncoding the checked encoding
		 * @return boolean accepted or not
		 */

		public function isEncodingAccepted($sEncoding) {

			$sEncoding = strtolower($sEncoding);

			return in_array($sEncoding, $this -> arrAcceptedEncodings)
			       ||
			       $this -> matchAttributePatterns($this -> arrAcceptedEncodings, $sEncoding);
		}


		/**
		 * Checks if the client accepts the given character set directly or as
		 * a match of a wildcard accept value
		 *
		 * @param string $sCharset the checked character set
		 * @return boolean accepted or not
		 */

		public function isCharsetAccepted($sCharset) {

			$sCharset = strtolower($sCharset);

			return in_array($sCharset, $this -> arrAcceptedCharsets)
			       ||
			       $this -> matchAttributePatterns($this -> arrAcceptedCharsets, $sCharset);
		}


		protected function matchAttributePatterns($arrAcceptPatterns, $sMatchValue) {

			// iterate all accept patterns
			foreach ($arrAcceptPatterns as $sAcceptPattern) {

				// dynamic check if the pattern contains an asterisk
				if (strpos($sAcceptPattern, '*') !== FALSE) {

					// build regular expression of this pattern
					$sAcceptPattern = preg_quote($sAcceptPattern, '~');
					$sAcceptPattern = str_replace('\*', '.*?', $sAcceptPattern);
					$sRegEx = "~^$sAcceptPattern\$~";

					// try to match regular expression
					if (preg_match($sRegEx, $sMatchValue)) {

						// if the pattern matches, the attribute is accepted
						return TRUE;
					}
				}

			}

			// nothing matched or no asterisks in the accept values
			return FALSE;
		}


		protected function parseAcceptFields($sInput) {

			$arrValues = array();
			$iN = 999;
			foreach (explode(',', $sInput) as $sValue) {

				// check if there is a weight given
				if (strpos($sValue, ';') !== FALSE) {

					// separate attribute and weight
					list($sAttribute, $sWeight) = explode(';', $sValue, 2);

					// match the weight value
					if (preg_match('~q=([0-9\.]+)~i', $sWeight, $arrRes)){

						// here are small values added.
						// this arranges same q-values in correct order
						// a;q=0.9,b;q=0.9 --> {a, b} not {b, a}
						$sWeight = floatval($sWeight) + ($iN-- / 100000);

						// apply the given, and slightly modified weight
						$arrValues[trim($sAttribute)] = $sWeight;
					}
					else {

						// some kind of parse error within the structure.
						// apply weight 0
						$arrValues[trim($sValue)] = 0 + ($iN-- / 100000);
					}
				}
				else {

					// if no special weight is given, weight 1 is applied
					$arrValues[trim($sValue)] = 1 + ($iN-- / 100000);
				}
			}

			// sort by the weight of the attributes
			arsort($arrValues);

			// return only the sorted attributes
			$arrReturn = array_keys($arrValues);

			// make all entries lowercase
			return array_flip(array_change_key_case(array_flip($arrReturn), CASE_LOWER));
		}
	}
